Skip to content

ffi: throw ERR_INVALID_ARG_TYPE for wrong-typed pointer and size - #65842

Open
soulee-dev wants to merge 2 commits into
nodejs:mainfrom
soulee-dev:ffi-arg-type-error-code
Open

ffi: throw ERR_INVALID_ARG_TYPE for wrong-typed pointer and size#65842
soulee-dev wants to merge 2 commits into
nodejs:mainfrom
soulee-dev:ffi-arg-type-error-code

Conversation

@soulee-dev

Copy link
Copy Markdown
Contributor

GetValidatedPointerAddress() and GetValidatedSize() throw ERR_INVALID_ARG_VALUE when the argument is not a bigint or not a number. ERR_INVALID_ARG_VALUE is reserved for a value of the right type that is invalid or unsupported; a wrong type is ERR_INVALID_ARG_TYPE. That is what the JavaScript validators behind exportString() and exportBuffer() throw, and what the inline IsBigInt() checks in toString(), toBuffer() and toArrayBuffer() throw for their first argument, so the same mistake currently gets a different code depending on which helper reports it:

$ node --experimental-ffi -p "try { require('node:ffi').exportBuffer(Buffer.alloc(1), 'x', 1) } catch (e) { e.code }"
ERR_INVALID_ARG_TYPE
$ node --experimental-ffi -p "try { require('node:ffi').toBuffer(1n, 'x') } catch (e) { e.code }"
ERR_INVALID_ARG_VALUE

Switch the two type-check branches to ERR_INVALID_ARG_TYPE. The branches that reject a negative or non-integer value, or a value outside the platform range, keep ERR_INVALID_ARG_VALUE and ERR_OUT_OF_RANGE. The messages are unchanged.

Affected paths

Call Before After
getInt8('x'), getInt8(undefined), getInt8() ERR_INVALID_ARG_VALUE ERR_INVALID_ARG_TYPE
setInt8('x', 0, 1), setInt8(undefined, 0, 1), setInt8() ERR_INVALID_ARG_VALUE ERR_INVALID_ARG_TYPE
getInt8(ptr, 'x'), setInt8(ptr, 'x', 1) ERR_INVALID_ARG_VALUE ERR_INVALID_ARG_TYPE
toBuffer(ptr, 'x'), toArrayBuffer(ptr, 'x'), toBuffer(ptr) ERR_INVALID_ARG_VALUE ERR_INVALID_ARG_TYPE
exportBuffer(buf, 'x', 1), exportArrayBuffer(ab, 'x', 1), exportArrayBufferView(view, 'x', 1) ERR_INVALID_ARG_VALUE ERR_INVALID_ARG_TYPE

The pointer argument of toString(), toBuffer(), toArrayBuffer() and exportString() is unaffected: those already reject a non-bigint with ERR_INVALID_ARG_TYPE before calling the helper. So is the len argument of the export* helpers, which validateInteger() checks in JavaScript first. No existing test asserted ERR_INVALID_ARG_VALUE on any of the affected paths.

The first commit is #65500 and is not part of this PR; this one depends on it because it updates the assertions that PR adds.

Refs: #65500
Refs: #65500 (comment)

ffi.getInt8() through ffi.getFloat64(), ffi.setInt8() through
ffi.setFloat64(), ffi.toBuffer() and ffi.toArrayBuffer() return
undefined instead of throwing when a required argument is omitted, so a
call that read or wrote nothing cannot be told apart from one that read
a zero byte. All 22 helpers behave this way.

GetValidatedPointerAddress() and GetValidatedSize() already reject the
same argument when it is passed explicitly as undefined. The
args.Length() test in front of them short-circuits the call and returns
Nothing without scheduling an exception. These six are the only tests in
src/ where args.Length() can skip a call that throws; the only other
Length() tests that guard a call at all guard Buffer::HasInstance(),
which cannot throw. The remaining tests in this file guard an inline
predicate and throw in the branch, which is why setUint8(ptr) reports
"Expected an offset argument" while setUint8() reports nothing at all.

Drop those tests. FunctionCallbackInfo::operator[] returns Undefined for
an out-of-range index, which is exactly the value these validators
reject, so each missing argument now produces the error its explicit
undefined counterpart produces. The documentation already describes this
behavior: the signatures are ffi.getInt8(pointer[, offset]),
ffi.setInt8(pointer, offset, value) and
ffi.toBuffer(pointer, length[, copy]), and the getters are documented to
return a number or a bigint.

ExportBytes() carried the same two tests. They are unreachable through
the public API because exportBytes is not exported and its three callers
all validate len in JavaScript first, but they are the same shape.

Signed-off-by: Soul Lee <alus20x@gmail.com>
GetValidatedPointerAddress() and GetValidatedSize() throw
ERR_INVALID_ARG_VALUE when the argument is not a bigint or not a
number. doc/api/errors.md reserves that code for a value of the right
type that is invalid or unsupported; a wrong type is
ERR_INVALID_ARG_TYPE. That is what the JavaScript validators behind
exportString() and exportBuffer() throw, and what the inline IsBigInt()
checks in toString(), toBuffer() and toArrayBuffer() throw for their
first argument. So ffi.exportBuffer(buf, ptr, 'x') reports a type error
while ffi.toBuffer(ptr, 'x') reports a value error for the same mistake.

Switch the two type-check branches to ERR_INVALID_ARG_TYPE. The branches
that reject a negative or non-integer value, or a value outside the
platform range, keep ERR_INVALID_ARG_VALUE and ERR_OUT_OF_RANGE.

This changes the code thrown for a non-bigint pointer by the getters,
the setters, exportBuffer(), exportArrayBuffer() and
exportArrayBufferView(), and for a non-number offset or length by the
getters, the setters, toBuffer() and toArrayBuffer(). The messages are
unchanged. No existing test asserted ERR_INVALID_ARG_VALUE on any of
these paths.

Refs: nodejs#65500
Signed-off-by: Soul Lee <alus20x@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/ffi

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. labels Sep 6, 2026
@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.19%. Comparing base (7aba44d) to head (3158b79).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
src/ffi/data.cc 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65842      +/-   ##
==========================================
- Coverage   90.20%   90.19%   -0.02%     
==========================================
  Files         771      771              
  Lines      264622   264619       -3     
  Branches    50228    50228              
==========================================
- Hits       238704   238666      -38     
- Misses      16907    16953      +46     
+ Partials     9011     9000      -11     
Files with missing lines Coverage Δ
src/ffi/data.cc 78.44% <87.50%> (+2.72%) ⬆️

... and 31 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trivikr trivikr added request-ci Add this label to start a Jenkins CI on a PR. author ready PRs with CI started, the required approvals, and no outstanding review comments. labels Sep 6, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Sep 6, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

nodejs-github-bot pushed a commit that referenced this pull request Sep 6, 2026
ffi.getInt8() through ffi.getFloat64(), ffi.setInt8() through
ffi.setFloat64(), ffi.toBuffer() and ffi.toArrayBuffer() return
undefined instead of throwing when a required argument is omitted, so a
call that read or wrote nothing cannot be told apart from one that read
a zero byte. All 22 helpers behave this way.

GetValidatedPointerAddress() and GetValidatedSize() already reject the
same argument when it is passed explicitly as undefined. The
args.Length() test in front of them short-circuits the call and returns
Nothing without scheduling an exception. These six are the only tests in
src/ where args.Length() can skip a call that throws; the only other
Length() tests that guard a call at all guard Buffer::HasInstance(),
which cannot throw. The remaining tests in this file guard an inline
predicate and throw in the branch, which is why setUint8(ptr) reports
"Expected an offset argument" while setUint8() reports nothing at all.

Drop those tests. FunctionCallbackInfo::operator[] returns Undefined for
an out-of-range index, which is exactly the value these validators
reject, so each missing argument now produces the error its explicit
undefined counterpart produces. The documentation already describes this
behavior: the signatures are ffi.getInt8(pointer[, offset]),
ffi.setInt8(pointer, offset, value) and
ffi.toBuffer(pointer, length[, copy]), and the getters are documented to
return a number or a bigint.

ExportBytes() carried the same two tests. They are unreachable through
the public API because exportBytes is not exported and its three callers
all validate len in JavaScript first, but they are the same shape.

Signed-off-by: Soul Lee <alus20x@gmail.com>
PR-URL: #65500
Fixes: #65499
Refs: #62072
Refs: #62858
Refs: #65842
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants